package org.apache.lucene.ocean.database;

import java.io.Reader;
import java.text.ParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.ocean.util.Constants;
import org.apache.lucene.ocean.util.NamedList;
import org.apache.lucene.ocean.util.NumberUtils;
import org.apache.lucene.ocean.util.Util;

/**
 * Container for OceanFields, extends NamedList which allow multiple fields of
 * the same name.
 * 
 */
public class OceanObject extends NamedList<String,OceanField> {
  public OceanObject() {
  }

  public String toString2() {
    StringBuilder b = new StringBuilder();
    Iterator<Map.Entry<String,OceanField>> iterator = iterator();
    while (iterator.hasNext()) {
      Map.Entry<String,OceanField> entry = iterator.next();
      String key = entry.getKey();
      OceanField field = entry.getValue();
      b.append(key);
      b.append(":");
      b.append(field.object.toString());
      if (iterator.hasNext()) {
        b.append(",");
      }
    }
    return b.toString();
  }

  public Object getValue(String name) {
    OceanField field = get(name);
    if (field == null)
      return null;
    return field.object;
  }

  public Long getVersion() {
    return (Long) getValue(Constants.VERSION);
  }

  public Long getId() {
    return (Long) getValue(Constants.ID);
  }

  public long getLong(String name) {
    Long v = ((Long) getValue(name));
    if (v == null)
      return -1;
    return v.longValue();
  }

  public OceanObject clone() {
    OceanObject oceanObject = new OceanObject(entries);
    return oceanObject;
  }

  private OceanObject(List<Map.Entry<String,OceanField>> entries) {
    super(entries);
  }

  /**
   * 
   * @param document
   *          Takes the document and creates OceanFields out of the fields
   * @throws ParseException
   */
  public OceanObject(Document document) throws Exception {
    List<Fieldable> fields = document.getFields();
    for (Fieldable field : fields) {
      String name = field.name();
      Object object = toExternalObject(name, field.stringValue());
      OceanField oceanField = new OceanField(name, object, getStore(field), getIndex(field), getTermVector(field), null);
      add(oceanField);
    }
  }

  /**
   * Converts the field object to an indexed version of the string. Used for
   * things like longs and doubles that if stored directly would not sort
   * correctly.
   * 
   * @param field
   * @return
   */
  public static String toInternalString(OceanField field) {
    if (Field.Index.UN_TOKENIZED == field.index.UN_TOKENIZED) {
      return toInternalString(field.name, field.object);
    } else {
      return (String) field.object;
    }
  }

  /**
   * The field may explicitly set the type by adding an underscore and the type
   * name. Example: battingaverage_double:321.55 otherwise if the object is a
   * number it is automatically converted into a double. If the object is a date
   * it is converted into an ISO 8601 date string like: 1995-12-31T23:59:59.999Z
   * 
   * @param field
   * @param object
   * @return
   */
  public static String toInternalString(String field, Object object) {
    if (object instanceof String) {
      return (String) object;
    }
    if (field.equals(Constants.ID) || field.equals(Constants.VERSION) || field.equals(Constants.DOCUMENTID)) {
      return NumberUtils.long2sortableStr((Long) object);
    }
    if (field.endsWith("_long") && object instanceof Long) {
      return NumberUtils.long2sortableStr((Long) object);
    }
    if (field.endsWith("_double") && object instanceof Double) {
      return NumberUtils.double2sortableStr((Double) object);
    }
    if (field.endsWith("_int") && object instanceof Integer) {
      return NumberUtils.int2sortableStr((Integer) object);
    }
    if (field.endsWith("_float") && object instanceof Float) {
      return NumberUtils.float2sortableStr((Integer) object);
    }
    if (field.endsWith("_date") && object instanceof Date) {
      return Util.formatDate((Date) object);
    }
    if (object instanceof Number) {
      //Number n = (Number) object;
      //return NumberUtils.double2sortableStr(n.doubleValue());
      return object.toString();
    }

    /**
     * if (object instanceof String) { String str = (String) object; // check to
     * see if it's parseable into a number/double try { double d =
     * Double.parseDouble(str); return toInternalString(field, d); } catch
     * (NumberFormatException nfe) { } // check to see if it's parseable into a
     * date try { Date date = Util.parseDate(str); return
     * toInternalString(field, date); } catch (ParseException pe) { } return
     * "s"+str; }
     */
    if (object instanceof Date)
      return Util.formatDate((Date) object);
    throw new RuntimeException("unknown type " + object.getClass().getName());
  }

  /**
   * Converts an indexed string into it's external value
   * 
   * @param field
   * @param string
   * @return
   * @throws ParseException
   */
  public static Object toExternalObject(String field, String string) throws Exception {
    try {
      if (field.endsWith("_long"))
        return new Long(NumberUtils.sortableStr2long(string));
      if (field.endsWith("_double"))
        return new Double(NumberUtils.sortableStr2double(string));
      if (field.endsWith("_int"))
        return new Integer(NumberUtils.sortableStr2int(string));
      if (field.endsWith("_float"))
        return new Float(NumberUtils.sortableStr2float(string));
      if (field.endsWith("_date"))
        return Util.parseDate(string);
      else
        return string;
    } catch (Exception exception) {
      throw new Exception("field:"+field+" string:"+string, exception);
    }
    // throw new RuntimeException("unknown type");
  }

  /**
   * Gets the Field.TermVector of the Fieldable
   * 
   * @param field
   * @return
   */
  public static Field.TermVector getTermVector(Fieldable field) {
    if (field.isTermVectorStored()) {
      if (field.isStoreOffsetWithTermVector() && field.isStorePositionWithTermVector())
        return Field.TermVector.WITH_POSITIONS_OFFSETS;
      if (field.isStoreOffsetWithTermVector())
        return Field.TermVector.WITH_OFFSETS;
      else if (field.isStorePositionWithTermVector())
        return Field.TermVector.WITH_POSITIONS;
      else
        return Field.TermVector.YES;
    }
    return Field.TermVector.NO;
  }

  /**
   * Get the Field.Index value for a Fieldable
   * 
   * @param field
   * @return
   */
  public static Field.Index getIndex(Fieldable field) {
    if (field.isIndexed()) {
      if (field.isTokenized())
        return Field.Index.TOKENIZED;
      if (field.getOmitNorms())
        return Field.Index.NO_NORMS;
      else
        return Field.Index.UN_TOKENIZED;
    }
    return Field.Index.NO;
  }

  /**
   * Get the Field.Store value for a Fieldable
   * 
   * @param field
   * @return
   */
  public static Field.Store getStore(Fieldable field) {
    if (field.isStored())
      return Field.Store.YES;
    else if (field.isCompressed())
      return Field.Store.COMPRESS;
    else
      return Field.Store.NO;
  }

  /**
   * Convert the OceanObject to a Lucene Document
   * 
   * @return
   */
  public Document toDocument() {
    Document doc = new Document();
    for (Map.Entry<String,OceanField> entry : this) {
      doc.add(entry.getValue().getFieldable());
    }
    return doc;
  }

  /**
   * Get the Analyzer for the OceanObject
   * 
   * @return
   */
  public Analyzer getAnalyzer() {
    Map<String,Analyzer> fa = new HashMap<String,Analyzer>();
    for (Map.Entry<String,OceanField> entry : this) {
      if (entry.getValue().analyzer != null) {
        fa.put(entry.getKey(), entry.getValue().analyzer);
      }
    }
    return new OceanAnalyzer(fa);
  }

  /**
   * Returns an Analyzer per field
   */
  public static class OceanAnalyzer extends Analyzer {
    Map<String,Analyzer> fa;

    public OceanAnalyzer(Map<String,Analyzer> fa) {
      this.fa = fa;
    }

    public TokenStream tokenStream(String field, Reader reader) {
      Analyzer analyzer = fa.get(field);
      if (analyzer == null)
        throw new RuntimeException("no analyzer for " + field);
      return analyzer.tokenStream(field, reader);
    }
  }

  /**
   * Add OceanField to this OceanObject
   * 
   * @param of
   */
  public void add(OceanField oceanField) {
    add(oceanField.name, oceanField);
  }
}
